package dev.ece.core.engine;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.GLFW_FALSE;
import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE;
import static org.lwjgl.glfw.GLFW.GLFW_TRUE;
import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE;
import static org.lwjgl.glfw.GLFW.glfwCreateWindow;
import static org.lwjgl.glfw.GLFW.glfwDefaultWindowHints;
import static org.lwjgl.glfw.GLFW.glfwDestroyWindow;
import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor;
import static org.lwjgl.glfw.GLFW.glfwGetVideoMode;
import static org.lwjgl.glfw.GLFW.glfwGetWindowSize;
import static org.lwjgl.glfw.GLFW.glfwInit;
import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent;
import static org.lwjgl.glfw.GLFW.glfwPollEvents;
import static org.lwjgl.glfw.GLFW.glfwSetCursorPosCallback;
import static org.lwjgl.glfw.GLFW.glfwSetErrorCallback;
import static org.lwjgl.glfw.GLFW.glfwSetFramebufferSizeCallback;
import static org.lwjgl.glfw.GLFW.glfwSetJoystickCallback;
import static org.lwjgl.glfw.GLFW.glfwSetKeyCallback;
import static org.lwjgl.glfw.GLFW.glfwSetMouseButtonCallback;
import static org.lwjgl.glfw.GLFW.glfwSetScrollCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowPos;
import static org.lwjgl.glfw.GLFW.glfwShowWindow;
import static org.lwjgl.glfw.GLFW.glfwSwapBuffers;
import static org.lwjgl.glfw.GLFW.glfwSwapInterval;
import static org.lwjgl.glfw.GLFW.glfwTerminate;
import static org.lwjgl.glfw.GLFW.glfwWindowHint;
import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose;
import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_LEQUAL;
import static org.lwjgl.opengl.GL11.GL_LINE_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_LINE_SMOOTH_HINT;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_NICEST;
import static org.lwjgl.opengl.GL11.GL_PERSPECTIVE_CORRECTION_HINT;
import static org.lwjgl.opengl.GL11.GL_POINT_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_POINT_SMOOTH_HINT;
import static org.lwjgl.opengl.GL11.GL_POLYGON_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_POLYGON_SMOOTH_HINT;
import static org.lwjgl.opengl.GL11.GL_SMOOTH;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL11.glClearDepth;
import static org.lwjgl.opengl.GL11.glDepthFunc;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glHint;
import static org.lwjgl.opengl.GL11.glLineWidth;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glPointSize;
import static org.lwjgl.opengl.GL11.glShadeModel;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;

import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.Version;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWImage;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.WGL;
import org.lwjgl.system.MemoryStack;

import dev.ece.core.clearup.ClearUpGLRubbishManager;
import dev.ece.core.engine.EngineEvent.Pointer;
import dev.ece.core.shader.Shader;
import dev.ece.core.texture.FBO;
import dev.ece.core.vertex.EBO;
import dev.ece.core.vertex.VAO;
import dev.ece.util.lib.Vector4f;
import dev.ece.util.tools.Screen;
import dev.ece.util.tools.TextureLoader;
import dev.ece.util.tools.Timer;

/**
 * 图形引擎引擎
 * 
 * @author 飘渺青衣 <b>E-mail：</b>3375758@qq.com
 */
public class Engine {
	
	private final Logger log = LogManager.getLogger(Engine.class);
	
	private static final ThreadLocal<Timer> TIMERTHREAD = new ThreadLocal<Timer>();

	private CountDownLatch latch = new CountDownLatch(3);
	
	private ExecutorService threadExecutor = Executors.newFixedThreadPool(2);
	
	/**
	 * 屏幕宽高信息
	 */
	private Screen screen;

	/**
	 * 图形引擎运转接口
	 */
	private EngineListener listener;
	
	/**
	 * 标题
	 */
	private String title;
	
	/**
	 * 键盘输入接口
	 */
	private EngineEvent engineEvent;
	
    private final Pointer pointer = new Pointer();
 
    private long window;
    
	private String version;
	
	private boolean isEngineCreated = false;
	
	private boolean logicLoop;
	
	private boolean loadThreadLoop;
	
	private static final int[] JIDS = new int[] {
		GLFW.GLFW_JOYSTICK_1,
		GLFW.GLFW_JOYSTICK_2,
		GLFW.GLFW_JOYSTICK_3,
		GLFW.GLFW_JOYSTICK_4,
		GLFW.GLFW_JOYSTICK_5,
		GLFW.GLFW_JOYSTICK_6,
		GLFW.GLFW_JOYSTICK_7,
		GLFW.GLFW_JOYSTICK_8,
		GLFW.GLFW_JOYSTICK_9,
		GLFW.GLFW_JOYSTICK_10,
		GLFW.GLFW_JOYSTICK_11,
		GLFW.GLFW_JOYSTICK_12,
		GLFW.GLFW_JOYSTICK_13,
		GLFW.GLFW_JOYSTICK_14,
		GLFW.GLFW_JOYSTICK_15,
		GLFW.GLFW_JOYSTICK_16
	};
	
	private static final Map<Integer, Integer> JID_INDEX = new LinkedHashMap<Integer, Integer>();
	
	static {
		for(int i=0; i<JIDS.length; i++) {
			JID_INDEX.put(JIDS[i], i);
		}
	}
	
	private Joystick[] joysticks;
	
	/**
	 * 背景颜色
	 */
	public final Vector4f backgroundColor = new Vector4f(0, 0, 0, 0);
	
	/**
	 * 构造函数
	 * @param graphicListener
	 */
	public Engine(
			EngineListener listener, 
			EngineEvent engineEvent) {
		this("Easy Cool Engine！", listener, engineEvent);
	}
	
	/**
	 * 构造函数
	 * @param graphicListener
	 */
	public Engine(
			String title,
			EngineListener listener, 
			EngineEvent engineEvent) {
		this.listener = listener;
		this.engineEvent = engineEvent;
		this.title = title;
		if(listener == null) {
			//抛出空指针异常
			throw new NullPointerException("Listener is null");
		}
		if(engineEvent == null) {
			throw new NullPointerException("engineEvent is null");
		}
	}
    
	private void init(int width, int height, boolean resizable) {
		this.version = Version.getVersion();
		
		GLFWErrorCallback.createPrint(System.err).set();

		// Initialize GLFW. Most GLFW functions will not work before doing this.
		if ( !glfwInit() )
			throw new IllegalStateException("Unable to initialize GLFW");
		
		// Configure GLFW
		glfwDefaultWindowHints(); // optional, the current window hints are already the default
		glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
		glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE: GLFW_FALSE); // the window will be resizable
		
		// Create the window
		window = glfwCreateWindow(width, height, this.title, NULL, NULL);
		if ( window == NULL )
			throw new RuntimeException("Failed to create the GLFW window");
		
		glfwSetFramebufferSizeCallback(window, (long window, int resize_width, int resize_height) -> {
			try {
				if(isEngineCreated) {
					if(resize_width > 0 && resize_height > 0) {
						onChange(resize_width, resize_height);
					}
				}
			} catch (Exception e) {
				log.error("窗口变换事件异常", e);
			}
		});
		// Setup a key callback. It will be called every time a key is pressed, repeated or released.
		glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
			try {
				if(isEngineCreated) {
					engineEvent.onKeyBoardEvent(window, key, scancode, action, mods);
				}
			} catch(Exception e) {
				log.error("按键事件异常", e);
			}
		});
		glfwSetJoystickCallback((int jid, int event)->{
			try {
				if(joysticks != null) {
					Integer index = JID_INDEX.get(jid);
					Joystick joystick = joysticks[index];
					if(event == GLFW.GLFW_CONNECTED) {
						joystick.setPresent(true);
						ByteBuffer buff = GLFW.glfwGetJoystickButtons(jid);
						joystick.setButtons(new byte[buff.capacity()]);
						
						FloatBuffer fbuff = GLFW.glfwGetJoystickAxes(jid);
						joystick.setAxes(new float[fbuff.capacity()]);
					    fbuff.get(joystick.getAxes());
					} else if(event == GLFW.GLFW_DISCONNECTED) {
						joystick.setPresent(false);
					}
				}
				engineEvent.onJoyStickEvent(jid, event);
			} catch(Exception e) {
				log.error("手柄连接事件异常", e);
			}
		});
		glfwSetCursorPosCallback(window, (long window, double x, double y)-> {
			pointer.setPosition((int) x, (int) y);
			try {
				if(isEngineCreated) {
					engineEvent.onMouseMoveEvent(window, pointer);
				}
			} catch(Exception e) {
				log.error("鼠标移动事件异常", e);
			}
        });
		glfwSetMouseButtonCallback(window, (long window, int button, int action, int mods) -> {
			try {
				if(isEngineCreated) {
					engineEvent.onMouseButtonEvent(window, button, action, mods, pointer);
				}
			} catch(Exception e) {
				log.error("鼠标按键事件异常", e);
			} 
        });
		glfwSetScrollCallback(window, (long window, double xoffset, double yoffset) -> {
			try {
				if(isEngineCreated) {
					engineEvent.onMouseScrollEvent(window, xoffset, yoffset, pointer);
				}
			} catch(Exception e) {
				log.error("鼠标滚动事件异常", e);
			}
        });
		// Get the thread stack and push a new frame
		try ( MemoryStack stack = stackPush() ) {
			IntBuffer pWidth = stack.mallocInt(1); // int*
			IntBuffer pHeight = stack.mallocInt(1); // int*

			// Get the window size passed to glfwCreateWindow
			glfwGetWindowSize(window, pWidth, pHeight);

			// Get the resolution of the primary monitor
			GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

			// Center the window
			glfwSetWindowPos(
				window,
				(vidmode.width() - pWidth.get(0)) / 2,
				(vidmode.height() - pHeight.get(0)) / 2
			);
		} // the stack frame is popped automatically
		// Make the OpenGL context current
		glfwMakeContextCurrent(window);
		// Enable v-sync
		glfwSwapInterval(1);
		// Make the window visible
		glfwShowWindow(window);
    }
	
	public void setIcon(BufferedImage icon) {
		GLFWImage image = GLFWImage.malloc();
        image.set(icon.getWidth(), icon.getHeight(), TextureLoader.loadImageToByteBuffer(icon));

        GLFWImage.Buffer images = GLFWImage.malloc(1);
        images.put(0, image);

        GLFW.glfwSetWindowIcon(window, images);

        images.free();
        image.free();
	}
	
	protected void startLoaderThread() {
		final long HDC = WGL.wglGetCurrentDC();
		final long HRC = WGL.wglGetCurrentContext();
		final long HRC2 = WGL.wglCreateContext(HDC);
		if(WGL.wglShareLists(HRC2, HRC)) {
			loadThreadLoop = true;
			this.threadExecutor.execute(new Runnable() {
	
				@Override
				public void run() {
					WGL.wglMakeCurrent(HDC, HRC2);
					GL.createCapabilities();
					while(loadThreadLoop) {
						//加载
						try {
							LoaderQueue.loading();
						} catch(Exception e) {
							log.error("加载线程异常", e);
						}
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							log.error("加载线程时停异常", e);
						}
					}
					WGL.wglDeleteContext(HRC2);
					WGL.wglMakeCurrent(NULL, NULL);
					latch.countDown();
				}
			});
		} else {
			log.error("共享渲染列表失败");
		}
	}
	
	/**
	 * 引擎开始工作
	 * @param width
	 * @param height
	 * @throws Exception 
	 */
	public void start(int width, int height) throws Exception {
		start(width, height, false);
	}
	
	/**
	 * 引擎开始工作
	 * @param width
	 * @param height
	 * @param resizable
	 * @throws Exception 
	 */
	public void start(int width, int height, boolean resizable) throws Exception {
		try {
            init(width, height, resizable);
            GL.createCapabilities();
            //创建加载线程
    		this.startLoaderThread();
            
            this.onCreate();
            
            this.startLogicThread();
            
            Timer timer = new Timer();
            TIMERTHREAD.set(timer);
            while (!glfwWindowShouldClose(window) ) {
                //时间
            	timer.begin();
            	//清屏
            	glfwSwapBuffers(window); 
    			//绘制
    			this.render();
    			//关闭
    			VAO.unbind();
				EBO.unbind();
				Shader.glUnUseProgram();
				FBO.finish();
    			//渲染结束
    			GL11.glFlush();
				GL11.glFinish();
    			//事件
    			glfwPollEvents();
    			pollJoystickEvents();
    			//计算时间
    			timer.testFinish();
    			timer.finish();
    			//清除垃圾
        		ClearUpGLRubbishManager.clearup();
            }
            TIMERTHREAD.remove();
            latch.countDown();
            logicLoop = false;
        	loadThreadLoop = false;
            try {
            	latch.await();
            } catch (InterruptedException e) {
            	log.error("线程await异常", e);
            }
            threadExecutor.shutdown();
            //摧毁程序结束
    		listener.onEngineDestroy();
    		//清除垃圾
    		ClearUpGLRubbishManager.clearup();
        } finally {
        	// Free the window callbacks and destroy the window
    		glfwFreeCallbacks(window);
    		glfwDestroyWindow(window);
    		// Terminate GLFW and free the error callback
    		glfwTerminate();
    		glfwSetErrorCallback(null).free();
        }
	}

	protected void startLogicThread() {
		logicLoop = true;
		this.threadExecutor.execute(new Runnable() {

			@Override
			public void run() {
				Timer timer = new Timer();
				TIMERTHREAD.set(timer);
				while (logicLoop) {
					timer.begin();
					try {
						getListener().onLogic(Engine.this);
					} catch (Exception e) {
						log.error("逻辑线程异常", e);
					}
					timer.testFinish();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						log.error("逻辑线程休息异常", e);
					}
					timer.finish();
				}
				TIMERTHREAD.remove();
				latch.countDown();
			}

		});
	}
	
	public static final Timer currentTimer() {
		return TIMERTHREAD.get();
	}
	
	/**
	 * 创建窗口
	 * @throws Exception 
	 */
	protected final void onCreate() throws Exception {
		// 启用阴影平滑
		glShadeModel(GL_SMOOTH);
		// 黑色背景
		glClearColor(
				this.backgroundColor.getRed(), 
				this.backgroundColor.getGreen(), 
				this.backgroundColor.getBlue(), 
				this.backgroundColor.getAlpha());
		// 设置深度缓存
		glClearDepth(1.0f);
		// 启用深度测试
		glEnable(GL_DEPTH_TEST);
		// 所作深度测试的类型
		glDepthFunc(GL_LEQUAL);
		// 开启背面
		glEnable(GL_CULL_FACE);
		// 告诉系统对透视进行修正
		//反走样
		glEnable(GL_POINT_SMOOTH);  
		glEnable(GL_LINE_SMOOTH); 
		glEnable(GL_POLYGON_SMOOTH);
		glLineWidth(10);
		glPointSize(10);
		//glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
		glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
		//反走样
		glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); // Make round points, not square points  
		glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);  // Antialias the lines
		glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);  // Antialias the lines  
		// 选择模型观察矩阵
		glMatrixMode(GL_MODELVIEW);
		// 重置当前的模型观察矩阵
		glLoadIdentity();
		//启用多重采样
		glEnable(GL13.GL_MULTISAMPLE);
		//初始化手柄设置
		this.initJoystick();
		this.listener.onCreate(this);
		this.isEngineCreated = true;
	}
	
	protected void initJoystick() {
		joysticks = new Joystick[JIDS.length];
		ByteBuffer buff = null;
		FloatBuffer fbuff = null;
		for(int i=0; i<joysticks.length; i++) {
			joysticks[i] = new Joystick();
			joysticks[i].setJid(JIDS[i]);
			joysticks[i].setIndex(i);
			joysticks[i].setPresent(GLFW.glfwJoystickPresent(JIDS[i]));
			if(joysticks[i].isPresent()) {
				buff = GLFW.glfwGetJoystickButtons(JIDS[i]);
			    joysticks[i].setButtons(new byte[buff.capacity()]);
			    buff.get(joysticks[i].getButtons());
			    
			    fbuff = GLFW.glfwGetJoystickAxes(JIDS[i]);
			    joysticks[i].setAxes(new float[fbuff.capacity()]);
			    fbuff.get(joysticks[i].getAxes());
			}
		}
	}

	protected void pollJoystickEvents() {
		ByteBuffer buff = null;
		FloatBuffer fbuff = null;
		byte[] buttons = null;
		float[] axes = null;
		Joystick joystick = null;
		for(int i=0; i<joysticks.length; i++) {
			joystick = joysticks[i]; 
			if(joystick.isPresent()) {
				buff = GLFW.glfwGetJoystickButtons(JIDS[i]);
				if(buff != null) {
					buttons = new byte[buff.capacity()];
					buff.get(buttons);
					for(int j=0; j<buttons.length; j++) {
						if(joystick.getButtons()[j] != buttons[j]) {
							joystick.getButtons()[j] = buttons[j];
							try {
								engineEvent.onJoyStickEvent(joystick, j, buttons[j]);
							} catch (Exception e) {
								log.error("手柄事件错误", e);
							}
						}
					}
				}
				fbuff = GLFW.glfwGetJoystickAxes(JIDS[i]);
				if(fbuff != null) {
					axes = new float[fbuff.capacity()];
					fbuff.get(axes);
					for(int j=0; j<axes.length; j++) {
						if(joystick.getAxes()[j] != axes[j]) {
							joystick.getAxes()[j] = axes[j];
							try {
								engineEvent.onJoyStickAxesEvent(joystick, j, axes[j]);
							} catch (Exception e) {
								log.error("手柄事件错误", e);
							}
						}
					}
				}
			}
		}
	}
	
	/**
	 * 屏幕转换
	 * 
	 * @param width
	 * @param height
	 * @throws Exception 
	 */
	protected final void onChange(int width, int height) throws Exception {
		screen = new Screen(width, height);
		listener.onScreenChange(this, screen, width, height);
	}

	/**
	 * 绘制
	 * @throws Exception 
	 */
	protected final void render() throws Exception {
		// 清除屏幕和深度缓存, 不然没有背景
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
		//绘制世界
		this.listener.onRender(this);
		//清除垃圾
		ClearUpGLRubbishManager.clearup();
	}

	public void exit() {
		glfwSetWindowShouldClose(window, true);
	}
	
	public Screen getScreen() {
		return screen;
	}

	public final EngineListener getListener() {
		return listener;
	}

	public final EngineEvent getEngineEvent() {
		return engineEvent;
	}

	public String getTitle() {
		return title;
	}

	public String getVersion() {
		return version;
	}
	
	public Pointer getPointer() {
		return pointer;
	}

	public long getWindow() {
		return window;
	}

}
